//uncomment for windows
//#include "Windows.h"
#include "GL/glut.h"
#include "GL/gl.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include "lib/stack.h"
#include "utils.h"
#include "matrix/matrix.h"
#include "drawing/line.h"
#include "drawing/vertex.h"


typedef enum M_MODE{
  M_PROJECTION, M_MODEL
}M_MODE;



void display();
void special();

void readFile();
void parseOrtho(char** tokens);
void parseFrustum(char** tokens);
void parseVertex(char** tokens);
void parseTranslation(char** tokens);
void parseRotation(char** tokens);
void parseScale(char** tokens);
void parseViewport(char**);
void parseClearColor(char**);
void parseSetColor(char**);


void pushMatrix();
void popMatrix();

void ortho(GLfloat l,GLfloat r,GLfloat b,GLfloat t,GLfloat n,GLfloat f);
void frustum(GLfloat l,GLfloat r,GLfloat b,GLfloat t,GLfloat n,GLfloat f);
void drawVertices();
void translate(GLfloat tx, GLfloat ty, GLfloat tz);
void scale(GLfloat sx, GLfloat sy, GLfloat sz);
void rotate(GLfloat angle, GLfloat vx, GLfloat vy, GLfloat vz);

GLint WINDOW_WIDTH = 600;
GLint WINDOW_HEIGHT=600;

//Set the filename here, or set to null to have it prompt you.
char* file_name = "Pr3Mi1Persp.txt";

VERTEX_2I view_ll;
VERTEX_2I view_ur;

MATRIX* model_matrix;
MATRIX* proj_matrix;

M_MODE matrix_mode = M_PROJECTION;

stack model_stack;

array_list vertex_matrices;

GLint rx = 0, ry = 0, rz = 0;

int main(int argc, char** argv){
  array_list_init(&vertex_matrices, 4);

  proj_matrix = malloc(sizeof(MATRIX));
  matrix_init(proj_matrix, 4, 4);
  matrix_load_identity(proj_matrix);
  model_matrix = malloc(sizeof(MATRIX));
  matrix_init(model_matrix, 4,4);
  matrix_load_identity(model_matrix);


  stack_init(&model_stack, 1);


  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_SINGLE);
  glutInitWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT);
  glutInitWindowPosition(100, 100);
  glutCreateWindow("JPGUNTER - Project3");
  glutDisplayFunc(display);
  glutSpecialFunc(special);
  glClearColor(1.0, 1.0, 1.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);
  gluOrtho2D(0, WINDOW_WIDTH-1, 0, WINDOW_HEIGHT-1);
  glutMainLoop();

  ortho(-1,1,-1,1,-1,1);
  return 0;
}

void special(int key, int x, int y) {
  printf("\nReceived input from key#: %d", key);
  switch (key) {

  case 100:
    ry -= 3;
    break;
  case 102:
    ry += 3;
    break;
  case 101:
    rx -= 3;
    break;
  case 103:
    rx += 3;
    break;

  case 104:
    rz += 3;
    break;
  case 105:
    rz -= 3;
    break;
  }
  glutPostRedisplay();

}

void display(){

  pushMatrix();
  rotate(rx, 1, 0 ,0);
  rotate(ry, 0, 1 ,0);
  rotate(rz, 0, 0 ,1);

  glClear(GL_COLOR_BUFFER_BIT);
  glColor3f(0,0,0);

  readFile();

  popMatrix();
  glFlush();
}

void readFile() {
  char* line = NULL;
  FILE* fp;
  char** tokens;
  int num_tokens = 1;
  int i = 0, j = 0;
  int skip_file = 0;



  if (file_name == NULL) {
    printf("\nEnter the name of the input file or 'n' to skip: ");
    line = getLine(stdin, 255);
    if(line[0] == 'N' || line[0] == 'n'){
      skip_file = 1;
    free(line);
    } else {
      fp = fopen(line, "r");
      printf("\nOpened \"%s\" for reading.", line);
      fflush(stdout);
    file_name = line;
    }
  } else {
    fp = fopen(file_name, "r");
    printf("\nOpened \"%s\" for reading.", file_name);
  }

  if(skip_file == 1){
    printf("\nSkipping file input.");
  } else if(fp != NULL ) {

    printf("\nStarting to parse the file.");
    fflush(stdout);

    line = getLine(fp, 100);

    while (line[0] != '\0') {
      i = 0;
      num_tokens = 1;
      j = 0;

      while (line[i] != '\0') {
        if (line[i] == ' ') {
          while(line[++i]== ' ');
          num_tokens++;
        }
        i++;
      }

      tokens = (char**) malloc(sizeof(char*) * num_tokens);

      tokens[j] = line;

      i = 1;
      while (line[i] != '\0') {
        if (line[i] == ' ') {
          line[i] = '\0';
          while(line[++i]== ' ');
          tokens[++j] = &line[i];
        }
        i++;
      }

      printf("\nparsing tokens:");
      for (i = 0; i < num_tokens; i++) {
        printf("%s : ", tokens[i]);
      }

      switch (tokens[0][2]) {
      case 'M':
        if(tokens[1][3] == 'P'){
          matrix_mode = M_PROJECTION;
        } else {
          matrix_mode = M_MODEL;
        }
        break;
      case 'O':
        parseOrtho(tokens);
        break;
      case 'F':
        parseFrustum(tokens);
        break;
      case 'V':
        if(tokens[0][3] == 'i'){
          parseViewport(tokens);
        } else {
          parseVertex(tokens);
        }
        break;
      case 'E':
        drawVertices();
        break;
      case 'L':
        if(matrix_mode == M_MODEL){
          matrix_load_identity(model_matrix);
        } else {
          matrix_load_identity(proj_matrix);
        }
        break;
      case 'T':
        parseTranslation(tokens);
        break;
      case 'R':
        parseRotation(tokens);
        break;
      case 'S':
        parseScale(tokens);
        break;
      case 'P':
        if(tokens[0][3] == 'u'){
          pushMatrix();
        } else {
          popMatrix();
        }
        break;
      case 'C':
        switch(tokens[0][7]){
        case '\0':
          glClear(GL_COLOR_BUFFER_BIT);
          break;
        case 'C':
          parseClearColor(tokens);
          break;
        case '3':
          parseSetColor(tokens);
          break;
        }
        break;
      }
      free(tokens);
      free(line);
      printf("\nParsed a line.");
      fflush(stdout);
      line = getLine(fp, 100);
    }

  }
  
  fclose(fp);
}

void parseOrtho(char** tokens){
  ortho(atof(tokens[1]),atof(tokens[2]),atof(tokens[3]),atof(tokens[4]),atof(tokens[5]),atof(tokens[6]));
}
void parseFrustum(char** tokens){
  frustum(atof(tokens[1]),atof(tokens[2]),atof(tokens[3]),atof(tokens[4]),atof(tokens[5]),atof(tokens[6]));
}

void parseVertex(char** tokens){
  VERTEX_3F v;
  MATRIX* m;
  m = malloc(sizeof(MATRIX));

  v.x = atof(tokens[1]);
  v.y = atof(tokens[2]);
  v.z = atof(tokens[3]);

  vertex3f_to_matrix(v,m);

  array_list_add(&vertex_matrices, m);
}

void parseTranslation(char** tokens){
  translate(atof(tokens[1]),atof(tokens[2]),atof(tokens[3]));
}
void parseRotation(char** tokens){
  rotate(atof(tokens[1]),atof(tokens[2]),atof(tokens[3]),atof(tokens[4]));
}
void parseScale(char** tokens){
  scale(atof(tokens[1]),atof(tokens[2]),atof(tokens[3]));
}

void parseViewport(char** tokens){
  view_ll.x = atoi(tokens[1]);
  view_ll.y = atoi(tokens[2]);
  view_ur.x = view_ll.x + atoi(tokens[3]);
  view_ur.y = view_ll.y + atoi(tokens[4]);

  printf("Viewport set to: (%d,%d) (%d,%d)", view_ll.x, view_ll.y,view_ur.x, view_ur.y);
}

void parseClearColor(char** tokens){
  glClearColor(atof(tokens[1]),atof(tokens[2]),atof(tokens[3]),atof(tokens[4]));
}
void parseSetColor(char** tokens){
  glColor3f(atof(tokens[1]),atof(tokens[2]),atof(tokens[3]));
}

void pushMatrix(){
  MATRIX* new_current;
  new_current = malloc(sizeof(MATRIX));
  matrix_init(new_current, model_matrix->w, model_matrix->h);
  matrix_copy(model_matrix, new_current);
  stack_push(&model_stack, model_matrix);
  model_matrix = new_current;
}
void popMatrix(){
  free(model_matrix);
  model_matrix = stack_pop(&model_stack);
}

void ortho(GLfloat l,GLfloat r,GLfloat b,GLfloat t,GLfloat n,GLfloat f){
  proj_matrix->data[0][0] = 2.0 / (r-l);
  proj_matrix->data[1][1] = 2.0 / (t-b);
  proj_matrix->data[2][2] = 2.0 / (f-n);
  proj_matrix->data[3][0] = (r+l)/(r-l);
  proj_matrix->data[3][1] = (t+b)/(t-b);
  proj_matrix->data[3][2] = (f+n)/(f-n);

}
void frustum(GLfloat l,GLfloat r,GLfloat b,GLfloat t,GLfloat n,GLfloat f){
  proj_matrix->data[0][0] = (2.0*n) / (r-l);
  proj_matrix->data[1][1] = (2.0*n) / (t-b);

  proj_matrix->data[2][0] = (r+l)/(r-l);
  proj_matrix->data[2][1] = (t+b)/(t-b);
  proj_matrix->data[2][2] = -1*(f+n) / (f-n);
  proj_matrix->data[2][3] = -1;

  proj_matrix->data[3][2] = (-2.0 * f * n)/(f-n);
  proj_matrix->data[3][3] = 0;

//  proj_matrix->data[0][0] = (r-l)/(2.0*n) ;
//  proj_matrix->data[1][1] = (t-b)/(2.0*n);
//
//
//  proj_matrix->data[2][3] = (-1*(f-n))/(2.0*f*n);
//
//  proj_matrix->data[3][0] = (r+l)/(2.0*n);
//  proj_matrix->data[3][1] = (t+b)/(2.0*n);
//  proj_matrix->data[3][2] = -1;
//  proj_matrix->data[3][3] = (f+n)/(2.0*f*n);
}

void drawVertices(){
  int i;
  GLfloat w,cx,cy,cz;
  MATRIX temp;
  VERTEX_2I first, last, current;
  matrix_init(&temp, 1,4);

  for(i=0;i<vertex_matrices.size;i++){
    matrix_mult(model_matrix, (MATRIX*)array_list_get(&vertex_matrices, i), &temp);
    matrix_copy(&temp, (MATRIX*)array_list_get(&vertex_matrices, i));\

    matrix_mult(proj_matrix, (MATRIX*)array_list_get(&vertex_matrices, i), &temp);
    matrix_copy(&temp, (MATRIX*)array_list_get(&vertex_matrices, i));

    //pull the coordinates out of the matrix.
    cx = ((MATRIX*)array_list_get(&vertex_matrices, i))->data[0][0];
    cy = ((MATRIX*)array_list_get(&vertex_matrices, i))->data[0][1];
    cz = ((MATRIX*)array_list_get(&vertex_matrices, i))->data[0][2];
    w = ((MATRIX*)array_list_get(&vertex_matrices, i))->data[0][3];

    //divide by w
    cx /= w;
    cy /= w;
    cz /= w;

    //convert to viewport coordinates.
    current.x = (int)(cx * (view_ur.x - view_ll.x)/2) + (view_ur.x - view_ll.x)/2 + view_ll.x;
    current.y = (int)(cy * (view_ur.y - view_ll.y)/2) + (view_ur.y - view_ll.y)/2 + view_ll.y;

    //draw the current line
    if(i != 0){
      drawline(current, last);
      if(i == vertex_matrices.size-1){
        //draw back to the first
        drawline(current, first);
      }
    }

    //set up for next iteration
    last.x = current.x;
    last.y = current.y;

    if(i == 0){
      first.x = last.x;
      first.y = last.y;
    }
  }

  array_list_init(&vertex_matrices, 4);
}

void translate(GLfloat tx, GLfloat ty, GLfloat tz){
  MATRIX trans, temp;

  matrix_init(&trans,4,4);
  matrix_load_identity(&trans);
  matrix_init(&temp,4,4);

  trans.data[3][0] = tx;
  trans.data[3][1] = ty;
  trans.data[3][2] = tz;
  matrix_mult(model_matrix, &trans, &temp);

  matrix_copy(&temp, model_matrix);
}
void scale(GLfloat sx, GLfloat sy, GLfloat sz){
  MATRIX scale, temp;

  matrix_init(&scale,4,4);
  matrix_load_identity(&scale);
  matrix_init(&temp,4,4);

  scale.data[0][0] = sx;
  scale.data[1][1] = sy;
  scale.data[2][2] = sz;

  matrix_mult(model_matrix, &scale, &temp);
  matrix_copy(&temp, model_matrix);
}
void rotate(GLfloat angle, GLfloat vx, GLfloat vy, GLfloat vz){
  MATRIX rot, temp;

  matrix_init(&rot,4,4);
  matrix_load_identity(&rot);
  matrix_init(&temp,4,4);

  if(vx == 1){
    rot.data[1][1] = cos(angle * (M_PI/180));
    rot.data[2][2] = cos(angle * (M_PI/180));
    rot.data[1][2] = sin(angle * (M_PI/180));
    rot.data[2][1] = -1 * sin(angle * (M_PI/180));
  } else if(vy == 1) {
    rot.data[0][0] = cos(angle * (M_PI/180));
    rot.data[2][2] = cos(angle * (M_PI/180));
    rot.data[2][0] = sin(angle * (M_PI/180));
    rot.data[0][2] = -1 * sin(angle * (M_PI/180));
  } else {
    rot.data[0][0] = cos(angle * (M_PI/180));
    rot.data[1][1] = cos(angle * (M_PI/180));
    rot.data[0][1] = sin(angle * (M_PI/180));
    rot.data[1][0] = -1 * sin(angle * (M_PI/180));
  }

  matrix_mult(model_matrix, &rot, &temp);

  matrix_copy(&temp, model_matrix);
}
